Skip to content

fix(platform-objects): translate the zh-CN object labels, options and help that shipped as English copies - #20490

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-20462-zh-cn-object-labels
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-20462-zh-cn-object-labels

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20462

Clause-②: no

What changed

A zh-CN console showed English on Setup surfaces: the Invite user dialog listed the membership roles as 所有者 / 管理员 / Delegated Admin / 成员, and a team record read MEMBER COUNT. The keys were present in packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts, but their values were byte copies of the en source, as os i18n extract --fill=default seeds them.

  • 320 copied leaves translated in the zh-CN objects bundle. Only leaf values changed; the structure is untouched. The two reproduced strings now read 受托管理员 (on both sys_member and sys_invitation) and 成员数.
  • 42 leaves kept in English by design, each declared with its reason in a decision ledger, objects-zh-cn-echo-decisions.test.ts. The list is below.
  • The provenance companion zh-CN.source-hashes.generated.ts was regenerated by pnpm i18n:extract, never by hand. It records a leaf only while the leaf is a byte copy of its source, so 320 rows dropped and 42 remain. The 42 are set-equal to the declared echoes.
  • Pin: the reproduced leaves sys_member.fields.role.options.delegated_admin, sys_invitation.fields.role.options.delegated_admin and sys_team.fields.member_count.label are pinned as translated under zh-CN. The role options must read one word per role on both objects.
  • ja-JP and es-ES are not touched. They carry the same copy class; their counts are under Out of scope.

The coverage-ratchet half is out of scope, per triage (5873680175). Teaching i18n-coverage-baseline to count a copied source string as untranslated widens an existing gate. If the maintainer wants that ratchet, it is a separate gate card. The new ledger deliberately does not assert that no other zh-CN leaf is a copy, for the same reason.

Copy count, before and after, by key class

A zh-CN string leaf counts as a copy when it is byte-equal to the en leaf at the same path, over every leaf of en.objects.generated.ts / zh-CN.objects.generated.ts. Both bundles have 1529 leaves.

key class before (b810ddb) after
label (object, field, view, action param) 196 27
options.* 20 9
pluralLabel 11 0
help 114 0
description 11 0
placeholder 4 4
emptyState.title / .message 4 0
result-dialog field (client.client_id, client.client_secret) 2 2
total 362 42
label + options.* 216 36

The card reads 338 in total, 192 of them label / options.*, at 3cf6449. The zh-CN and en object bundles are byte-identical between 3cf6449 and b810ddb, and this walk reads 362 / 216 at 3cf6449 too. So the difference of 24 comes from the counting method, and the card does not state its method.

help / description / placeholder / emptyState are decided as the same class as label. The extractor files each of them as an ExpectedEntry under the same source kind as its element's label (field, object, view, action; packages/cli/src/utils/i18n-extract.ts), and the coverage detector consumes them identically. So they were decided as the same class: help, description and emptyState were translated, and all four placeholders are declared echoes, because each shows a value the admin types (see the list below).

Kept in English by design (42), with reasons

leaves value(s) reason
sys_account._actions.link_social.params.provider.options.{google, github, microsoft, apple, facebook, gitlab, discord} (7) Google, GitHub, Microsoft, Apple, Facebook, GitLab, Discord Sign-in provider brands, shown on the link button as the name a user recognises. The bundle renders no brand name: the authored SSO empty state keeps Okta, Entra and Auth0 verbatim.
fields.id.label on sys_oauth_application, sys_oauth_access_token, sys_oauth_refresh_token, sys_oauth_consent, sys_oauth_resource, sys_oauth_client_resource, sys_oauth_client_assertion, sys_sso_provider, the eight sys_scim_* objects, sys_email_template, sys_metadata, sys_metadata_history, sys_view_definition, sys_metadata_audit, sys_secret, sys_setting_audit (23) ID The bundle keeps the initialism verbatim in every label that carries it (用户 ID, 团队 ID, 客户端 ID). A bare ID has no noun to render, and adding one would invent content.
sys_oauth_application.fields.jwks.label, .jwks_uri.label (2) JWKS, JWKS URI Protocol names the bundle keeps verbatim: sys_jwks is authored 签名密钥 (JWKS), and URIs stay URI (重定向 URI). Their help leaves are translated.
sys_oauth_application._actions.create_oauth_application.resultDialog.fields.client.client_id / .client_secret (2) Client ID, Client Secret The credential names an admin copies out of the one-time dialog. The same dialog family authored its zh-CN text with the names verbatim (我已保存 Client Secret, 新的 Client Secret).
sys_sso_provider._actions.register_saml_provider.params.entryPoint.label (1) IdP SSO URL Nothing but protocol initialisms the bundle keeps verbatim. The sibling SAML params are authored IdP 实体 ID and IdP 签名证书.
sys_sso_provider._actions.register_sso_provider.params.{scopes, mapEmail, mapName}.placeholder (3) openid email profile, email, name A placeholder shows the literal value the admin types: OIDC scope and claim names the IdP matches byte for byte. The authored help on the same params keeps them verbatim (默认为 “email”).
sys_sso_provider._actions.register_saml_provider.params.identifierFormat.placeholder (1) urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress The SAML NameID format URN the admin types: an identifier, not prose.
sys_email.fields.message_id.label (1) Message-ID The RFC 5322 header name. The authored help on the same field keeps it (传输层分配的 RFC-5322 Message-ID).
sys_setting_audit.fields.source.options.{ui, api} (2) UI, API Initialisms the bundle keeps wherever they appear (API 密钥, 在 UI 中暴露).

Where the declaration lives. Neither the extract config nor os i18n extract / os i18n check has a keep-source mechanism: no allowlist, no flag, no provenance mark. --filter only narrows emission. The existing mechanism that declares a leaf "English by design" is this package's echo-decision ledger. The shape is type Verdict = 'translate' | 'echo', with a reason on every row and a departure reason required for every echo (undeclaredEchoes). It lives in packages/platform-objects/src/apps/translations/*-echo-decisions.test.ts, one file per family. report-dataset-panel-echo-decisions.test.ts is the first: it has Verdict, a reason per row and the per-locale departure check written inline. The named undeclaredEchoes predicate arrives in the later rounds, first in object-field-editor-panel-echo-decisions.test.ts. objects-zh-cn-echo-decisions.test.ts is that shape applied to the objects bundle. No new mechanism was added.

Terminology

The terms follow the zh-CN leaves that were already authored, so one term is not translated two ways:

  • delegated_admin is 受托管理员. That is the console's own word for this role value (objectui organization.roles.delegatedAdmin). The other three roles already read the same in both places (所有者 / 管理员 / 成员). The alternative is 委派管理员, the in-repo word for the ADR-0090 concept (委派管理范围). One line changes it if the maintainer prefers it.
  • SCIM provisioning is 预配, following sys_scim_group_member.display_title (预配的用户及其所在组). Decommission is 下线.
  • The notification leaves take the service-messaging bundle's words: 主题, 严重程度 (信息 / 警告 / 严重), 负载, 去重键 and 通道.
  • managed_by options take sys_metadata's words: 管理方, 平台 / 包 / 管理员.
  • OIDC claims stay claim, and help text keeps scopes, both as the bundle already writes them (映射:邮箱 claim; 客户端可申请的 scopes …).
  • "My Memberships" is 我的组织, the account nav's label for the same screen. Web is Web 应用, like the sibling sys_oauth_application.fields.type.options.web.
  • sys_migration keeps its section's ASCII punctuation.

Collateral: nine existing ledgers encoded the old copy debt

Nine panel ledgers in the same directory asserted floors on the zh-CN provenance table: more than 100 rows, more than 300, and more than 50 echoing objects leaves "to sample". Those floors held only while about 360 zh-CN objects copies existed, so any card that translated them would go red. Each floor's own message states its intent, "provenance table is empty" or "has no echoing objects leaf to sample". Each now asks for greater than 0, with a one-line comment saying why.

One more test pinned sys_oauth_resource.fields.access_token_ttl.label as an unauthored fill in all three locales. Its own message says "if a translator authored it, this ledger note is stale". It now records that zh-CN is authored (访问令牌 TTL keeps the TTL token, the treatment the lifecycle rows give it), while ja-JP and es-ES stay fills.

Files: action-body-panel-, bare-type-display-, dataset-panel-, field-panel-, hook-execution-panel-, object-collapsed-sections-, object-lifecycle-panel-, page-interface-panel- and report-form-echo-decisions.test.ts. PR #20485 edits three of these files (field-panel-, object-collapsed-sections-, object-lifecycle-panel-) in different hunks. A local merge-tree run was not taken, so whether the two merge cleanly is not measured.

Verification

Measured at head 08ebcd01d. That is a true merge of origin/main 851af0c, and the CLI closure was rebuilt after the merge.

  • pnpm check:i18n (H3):
    • exit 0 at base;
    • exit 1 after the leaf edits alone, reported as platform-objects DRIFTED (1). The drifted file was the provenance companion;
    • exit 0 after pnpm i18n:extract. The extract changed only zh-CN.source-hashes.generated.ts: −320 rows, 0 added, and the bundle file stayed byte-identical.
  • pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2: 56 files and 921 tests passed. Before the collateral edit, the same run had 15 failures across 9 files, all of them the floors above.
  • pnpm --filter @objectstack/platform-objects typecheck: exit 0. tsconfig.test.json includes the new ledger (--listFilesOnly: 1 hit).
  • Reverse verification. Three mutation legs, each through scripts/ablation-replace.mjs on zh-CN.objects.generated.ts, each red as predicted, and each restored to blob 24cfb9355b89 with git diff HEAD empty:
    1. label: "成员数" changed back to "Member Count": 1 failed / 9 passed, "sys_team.fields.member_count.label reads its en source again".
    2. Both delegated_admin: "受托管理员" changed back to "Delegated Admin": 2 failed / 8 passed. The verdict row failed, and so did the role-word test ("expected 'Delegated Admin' to match /\p{Script=Han}/u").
    3. The declared echo google: "Google" changed to "谷歌": 1 failed / 9 passed, "is a declared echo and must stay the en source".
  • Derived gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 08ebcd01d printed 61 commands. All 61 were run, and each exit code was written to a file before any pipe.
    • 60 exited 0 on their first run.
    • pnpm check:dual-build-cjs-loads first answered PREREQUISITE NOT MET (exit 3: workspace dist/ missing for packages outside the CLI closure). That is not a measurement. Rerun once the dist/ existed: exit 0.
    • --ran reconciliation: 61 derived, 61 run, 0 NOT-MEASURED, 0 UNRUN (exit 0).
    • pnpm check:i18n-coverage: exit 0, "621 baselined untranslated string(s), none new". The ratchet is unchanged, as expected.
    • NOT MEASURED locally, declared to CI: the 6 workflow-valued families, the 5 path-scheduled CI jobs and the 4 type-check lanes the derivation names.

Patch round 1

The contract review 5876239898 (PASS at 08ebcd01d) named one false word and two comment slips. All three are corrected here, as text only; no translated value, assertion or floor changed.

  • Changeset: the translated-class list no longer names placeholders. All four placeholders were decided as echoes, and none was translated.
  • Ledger header (objects-zh-cn-echo-decisions.test.ts, comment only): it no longer says the provenance table "kept exactly the 42" as an assertion. It now names what is asserted: every declared echo still has its provenance row, and no pinned translation has one. That the table holds nothing beyond the echoes is measured, and deliberately not asserted.
  • This body: the H4 sentence now says placeholders were decided as the same class and are all declared echoes. The ledger-history sentence now credits undeclaredEchoes to the later rounds rather than to report-dataset-panel-.
  • Merge and readings: origin/main fc0db22 was merged with a true merge commit. At head 927957df4, pnpm check:i18n exited 0 and pnpm --filter @objectstack/platform-objects exec vitest run src/apps/translations/objects-zh-cn-echo-decisions.test.ts passed 10 of 10 (lock verdict: command-exit 0). The round-0 gate readings above were taken at 08ebcd01d and were not re-run.

Out of scope

  • The coverage ratchet (see above).
  • ja-JP and es-ES carry the same copy class, measured at b810ddb and not edited:
    • ja-JP: 383 copies (label 210 · options.* 20 · pluralLabel 12 · help 121 · description 12 · placeholder 4 · emptyState 4); label + options.* = 230.
    • es-ES: 392 copies (label 217 · options.* 22 · pluralLabel 12 · help 121 · description 12 · placeholder 4 · emptyState 4); label + options.* = 239.

Acceptance notes

  • The existing zh-CN bundle renders Locked Until two ways, 锁定至 on sys_user and 锁定到 on sys_job_queue. This card uses 锁定至 for sys_two_factor and leaves the pre-existing pair alone.

Generated by Claude Code

… help that shipped as English copies

Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN
Co-authored-by: Claude <noreply@anthropic.com>
…ter the translations (pnpm i18n:extract)

The extractor drops a leaf's copy record once the leaf is no longer a byte
copy of its source: 320 rows go, the 42 leaves kept in English stay.

Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN
Co-authored-by: Claude <noreply@anthropic.com>
…sh and pin the reproduced strings

A decision ledger for the objects bundle, in the shape the metadata-form
ledgers use: 42 leaves declared English by design with a reason each, and
the two reproduced strings pinned translated. The nine panel ledgers' zh-CN
provenance floors assumed the old copy debt (more than 100 or 300 rows, more
than 50 echoing objects leaves); they now ask for a non-empty table and at
least one sample, and the Access Token TTL near-twin records that zh-CN is
now authored.

Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/platform-objects, touching 32 documentable anchor(s).

29 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json fc0db22bcfdbdb778945317fc4de6dc46aab966d.

⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: sys_user (symbol, 38 pages)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json fc0db22bcfdbdb778945317fc4de6dc46aab966d → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1a7ebc6e875a2f266885c0eb9e33746d52d3cd6c — the merge of head 927957df4e375e0d7c338d7e63e859335b643d3c into base fc0db22bcfdbdb778945317fc4de6dc46aab966d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 1a7ebc6e875a2f266885c0eb9e33746d52d3cd6c && git checkout 1a7ebc6e875a2f266885c0eb9e33746d52d3cd6c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fc0db22bcfdbdb778945317fc4de6dc46aab966d 927957df4e375e0d7c338d7e63e859335b643d3c && git checkout -B drift-repro fc0db22bcfdbdb778945317fc4de6dc46aab966d && git merge --no-ff 927957df4e375e0d7c338d7e63e859335b643d3c

node scripts/docs-audit/affected-docs.mjs --json fc0db22bcfdbdb778945317fc4de6dc46aab966d

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs fc0db22bcfdbdb778945317fc4de6dc46aab966d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 08ebcd01dcc490e4c7948be77e9c50ff2d1b4c7c
Local-runs: none

Inputs, and nothing else: card #20462 body and all four comments (triage 5873680175, claim 5875144601, os-dev-report 5875946281, amended claim 5875978969); PR #20490 body, its 13-file list and the net diff against merge-base 851af0c27 (read-only git fetch, git show, git diff); the check-runs on the head, read twice (first read: 12 in progress; final read 2026-09-28T18:35:07Z, below); PR #20485 metadata and its patches on the three shared ledgers. objectui packages/i18n/src/locales/zh.ts read for the console words. The four locale bundles and the provenance table were parsed from blobs in scratch (python over git show output). No worktree, checkout, build, test, gate or ablation.

① Derived judgments

  1. Bundle zh-CN.objects.generated.ts: 320 leaf values change, 0 keys. Key set identical to en and unchanged from base to head (1529 leaves each, measured). Every changed leaf was byte-equal to en at base, none is at head, every new value carries Han. RIGHT: leaf values only, as the claim requires; check:i18n is green on the head (Lint & Repo Gates).
  2. Provenance companion zh-CN.source-hashes.generated.ts: −320 rows, +0, 42 left, rows sorted. At base and at head every row's digest equals hashSource(en leaf) under the repo's own FNV-1a-plus-finalizer (source-hash.ts), recomputed in scratch (hashSource("ID") = 00b0385c9c152888, as the file shows); rows equal the byte-copies at both revisions (362 and 42); the 320 dropped rows are exactly the 320 changed leaves. RIGHT: a faithful pnpm i18n:extract output, no hand edit detectable.
  3. The 42 echoes, each judged. 7 sign-in brands (Google … Discord): right. 23 bare ID labels: the bundle keeps ID inside 用户 ID / 团队 ID / 客户端 ID, and a noun-less ID has nothing to render; right. JWKS, JWKS URI: protocol names, and their help leaves are translated (rows 72–73 of the diff); right. IdP SSO URL: right (siblings authored IdP 实体 ID / IdP 签名证书). Four placeholders (openid email profile, email, name, the SAML NameID URN): typed literals the admin types; right. Message-ID: RFC 5322 header name; right. UI / API: initialisms; right. Result-dialog Client ID / Client Secret: defensible and disclosed — the same dialog family authors 我已保存 Client Secret and 新的 Client Secret verbatim (both present in the bundle), so the one-time credential dialog keeps the names. It is the one place Client ID reads English while seven field labels read 客户端 ID; a judgment the maintainer can flip in one row. No ordinary label hides among the 42: the set is equal to the provenance rows and to the measured copies, checked three ways.
  4. Ledger objects-zh-cn-echo-decisions.test.ts: the family shape (Verdict = 'translate' | 'echo', a reason per row, a departure per echo, undeclaredEchoes — present in ten sibling ledgers), no new mechanism. It pins the three reproduced leaves as translated with Han, pins role-option equality between sys_member and sys_invitation with distinct Han words, and pins every echo to its provenance row; tsconfig.test.json includes src/**/*, so it is in the test type-check program. RIGHT. Two comment-accuracy nits, non-blocking: the header says the table "kept exactly the 42 below; asserted", while the tests assert echoes ⊆ table and translate-rows ∉ table, not table ⊆ echoes (by design, per the PR's own "does not assert" paragraph; the equality is measured true at head); and the PR body names report-dataset-panel-echo-decisions.test.ts as the first ledger with undeclaredEchoes, while that file has Verdict and the predicate arrives in the later rounds.
  5. Translations: all 320 changed leaves were read, not a sample, including every options.* value (managed_by admin / package / platform, delegated_admin on both objects, lock_state no-delete / no-overlay, severity critical / info / warning, type web) and every leaf on sys_member, sys_invitation, sys_team, sys_team_member, sys_session and sys_two_factor. No mistranslation found. Consistency was measured mechanically: grouping all 1529 leaves by en value, the head renders an en value two ways in only two cases that involve a changed leaf, both disclosed — Client ID (above) and Locked Until (锁定至 on sys_user and sys_two_factor vs the pre-existing 锁定到 on sys_job_queue); 33 such pairs already exist at base and this PR adds none. Terms follow in-bundle precedent: 预配 (1 to 21), 下线 (0 to 10), 通道 (0 to 7), 范围 for scopes (matches four existing labels), 启用 for Active (all eight agree), 过期时间 (all ten agree), 创建时间 / 更新时间 (44 / 30 agree), 上级组织 (existing), 管理方 with 平台 / 包 / 管理员 (sys_metadata's words), 严重程度 / 去重键 / 负载 / 主题 (present in service-messaging's zh-CN bundle), 我的组织 (the hand-authored zh-CN.ts:22 nav_account_memberships label), Web 应用 (the sibling option), and sys_migration's ASCII punctuation (11 precedents in that section). objectui agreement: zh.ts:2800 delegatedAdmin: '受托管理员', roles 所有者 / 管理员 / 成员, 岗位 for positions, 业务单元, 过期时间. Stylistic nits only: Last Login At was 最近登录时间 while the new Last Activity At is 最后活动时间 (different en terms, so not a two-way rendering); the new emptyState uses 你 where the bundle already mixes 你 (4) and 您 (8).
  6. The nine panel ledgers: every hunk is one floor line plus a two-line comment, and the TTL near-twin. The old floors (greater than 100 / 300 provenance rows, greater than 50 echoing objects leaves) pinned the old copy debt — each fails at 42, as the dev's pre-fix run reported (15 failures across 9 files). The new floors are greater than 0 with the same message ("provenance table is empty" / "has no echoing objects leaf to sample"): still a real lit-check an empty import cannot pass, and every assertion that follows (stillFilled equals [], metadataFormRows equals [], every echoing leaf has a row) keeps full strength; the paired authored.length greater-than-50 floors are untouched. The loop runs over all three locales, so ja-JP / es-ES (383 / 392 rows) also drop to greater than 0 — exactly what the amended claim authorises ("each floor becomes non-emptiness") and nothing beyond it. TTL near-twin: zh-CN is now asserted authored (not equal to en, carries TTL via carriesToken, defined at line 851, no provenance row); ja-JP / es-ES keep the fill assertion. It reads true against the head (leaf 访问令牌 TTL, row absent). Nothing is weakened beyond what the translation requires. RIGHT.
  7. H1 numbers: the method is stated (byte-equal string leaves at the same path over all leaves, 1529 per bundle) and reproduces exactly from the base blobs: 362 total (label 196, options 20, pluralLabel 11, help 114, description 11, placeholder 4, emptyState 4, result-dialog 2), label plus options 216; ja-JP 383 / 230 and es-ES 392 / 239 with the stated per-class split. The en and zh-CN bundle blobs are identical across 3cf6449, b810ddb, 851af0c and the PR base 3062e50. The card's 338 / 192 matches no variant tried (excluding the bare ID labels gives 339 / 193; excluding _actions and _views gives 329 / 193); the card states no method, so the PR's finding stands.
  8. H4 (help / description / emptyState translated beyond the claim's label / options.* wording): the extractor files a field's help and placeholder under source 'field' (packages/cli/src/utils/i18n-extract.ts:1192-1193), the same class as its label. RIGHT to translate, and the count table records it. Placeholders: none was translated (4 before, 4 after; all four are declared echoes) — see ② for the sentence this falsifies.

Gate verdicts, final read 2026-09-28T18:35:07Z: 34 check-runs, 31 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke — path / opt-in skips), 0 in progress, 0 failed. Lint & Repo Gates (runs check:i18n, check:i18n-coverage, check:i18n-stale-fill, check:i18n-walk-parity) success at 18:31:24Z; Test Core 1/6 – 6/6 success (the package's vitest suite, including the ten ledgers); Type Check workspace / source / consumer / debt-ledger success; Check Changeset, Governed Surface Queue Guard, Dogfood Regression 1–3, Dogfood Verify CLI, Temporal Conformance, Build Core success.

② Semver level

.changeset/20462-zh-cn-object-labels.md declares @objectstack/platform-objects: patch. RIGHT: the diff changes 320 string values inside a published generated bundle and drops 320 rows from its provenance companion; no export, type, key, schema or API moves, and nothing a consumer's code binds to changes. A copy fix in user-visible text is a patch. The Clause-② line reads no in both the PR body and the changeset; right — no forward-compat lever, flag or removal is involved.

Changeset sentences: the title sentence TRUE (the 320 copies are gone; the 42 that remain are declared, as the third bullet says). The symptom sentence TRUE per the card (not re-measured in a browser). "byte copies of the English source that os i18n extract --fill=default seeds" TRUE (--fill=default is in the root i18n:extract script; byte-equal at base). "320 of the 362 … are now translated: labels, plural labels, select options, help, descriptions, placeholders and empty states" TRUE in its numbers, FALSE in its class list: zero placeholders were translated (placeholder 4 to 4; all four are echoes). "delegated_admin reads 受托管理员 on both … the word the console already uses" TRUE (objectui zh.ts:2800). "The other 42 stay English by design, each recorded with its reason" TRUE (42 echo rows, each with a departure). The provenance bullet ("regenerated by pnpm i18n:extract … now lists exactly those 42") TRUE, measured. "ja-JP and es-ES are unchanged" TRUE (blobs identical base to head).

PR-body sentences: "Fixes #20462" TRUE. The what-changed bullets (320 / 42 / regenerated / pin / ja-JP and es-ES untouched) TRUE, each measured above; "The 42 are set-equal to the declared echoes" TRUE. The coverage-ratchet paragraph TRUE (baseline untouched; the ledger carries no table ⊆ echoes assertion). The count table: every cell TRUE; "Both bundles have 1529 leaves" TRUE; "byte-identical between 3cf6449 and b810ddb, and this walk reads 362 / 216 at 3cf6449 too" TRUE; "the card does not state its method" TRUE. "help / description / placeholder / emptyState are decided as the same class as label … So they were translated too" TRUE for help / description / emptyState, FALSE for placeholder (decided, all four as echoes; none translated). The echo table: every row's leaves, values and reasons TRUE against the bundle (the two Client Secret strings and the term counts were measured directly; the other quoted precedents were not each grepped individually, and the rows are judged right on their own terms). "Where the declaration lives": no keep-source mechanism in i18n-extract.config.ts (a plain defineStack) or in os i18n extract / check (only --filter and --fill) TRUE; the ledger shape TRUE; "report-dataset-panel-… is the first" TRUE as the first file, though undeclaredEchoes is not in it (nit in ①.4). The terminology bullets TRUE (each precedent found). The collateral section TRUE (floors and messages as described, nine files named correctly, "#20485 edits three of these files in different hunks" TRUE, "A local merge-tree run was not taken" TRUE of the dev; measured here, ③.9). The verification section's local runs are the dev's and are not re-measured here; every CI-declared family is green on the head. The out-of-scope numbers TRUE, measured. The acceptance note on Locked Until TRUE.

③ Boundary flags

  1. Dev open question, the zh-CN word for delegated_admin: answered A (受托管理员) by the seat in 5875978969. The diff carries it on both leaves, objectui zh.ts:2800 uses the same word, and the ledger pins equality across the two objects. Closed.
  2. H2 (no keep-source mechanism; the echo ledger used under "or wherever that mechanism lives"): verified as stated; the seat admitted it. Closed.
  3. H3 (provenance drift regenerated by pnpm i18n:extract): verified generated (digests, order, row set). Closed.
  4. H4 (help / description / emptyState translated beyond the claim's label / options.* wording): the amended claim read the report (Thread-read 5875946281) without objecting; the extractor class is one, the card's Expected names user-facing labels, and the count table records it. Answered: accepted, no escalation.
  5. The undeclared widening to the nine ledgers: the seat admitted it, floors only, and the diff holds to that (floor lines, comments, the TTL near-twin). Closed.
  6. The first commit amended before its first push (trailer swap): a process note; "The card this PR closes must claim this branch" and the trailer gate are green. No action.
  7. Out-of-scope finding (class a, reach: same public door): ja-JP 383 and es-ES 392 copies, with Delegated Admin on both sys_member and sys_invitation in both locales — measured TRUE from the base blobs, and the Invite dialog's role list is served from these bundles. Recommendation: the seat files one follow-up card (or one per locale) with reach: same public door as the card, the dev's dedupe words, and the console's existing words as the anchor (objectui ja.ts 委任管理者, es.ts Administrador delegado). Not a blocker here.
  8. The "Locked Until" two renderings: pre-existing on sys_user (锁定至) vs sys_job_queue (锁定到); this PR sides with the identity-side word. Agreed with "noted, not filed": it is one of 33 en values already rendered two ways at base, and this PR adds none. A zh-CN consistency sweep would be its own card if the maintainer wants one.
  9. PR feat(spec): curated activityMilestones, publicSharing, userActions and inlineColumns form rows (#19332, flight G2b) #20485 overlap (mergeability): both PRs start from the same blobs of the three files (verified by blob id at 851af0c and at feat(spec): curated activityMilestones, publicSharing, userActions and inlineColumns form rows (#19332, flight G2b) #20485's base 9801da1). feat(spec): curated activityMilestones, publicSharing, userActions and inlineColumns form rows (#19332, flight G2b) #20485's hunks sit at lines 665 / 726 (field-panel), 905 (collapsed-sections) and 1137 (lifecycle); this PR's at 557, 770–854 and 1283–1350. A three-way git merge-file over each blob triple in scratch exits 0 with no conflict markers: textually mergeable, and the later lander merges main per the seat's ruling. Not blocking.
  10. Changeset wording: one word overstates ("placeholders" in the translated-class list), and the PR body's "So they were translated too" carries the same slip. Non-blocking; if the branch is re-pushed for any other reason, drop the word.

Implemented-by: claude/issue-20462-zh-cn-object-labels
Reviewed-by: session_01N8TPEsoJxPsdSdNKGnNGEN

VERDICT: PASS

…s, and the ledger header says what it asserts

All four placeholders were decided as echoes, so the changeset's translated
class list drops them. The ledger header now names the two assertion
directions instead of claiming the provenance rows equal the echoes.

Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 927957df4e375e0d7c338d7e63e859335b643d3c
Local-runs: none

Delta review on top of PASS 5876239898 at 08ebcd01d. Inputs added, and nothing else: the patch-round-1 os-dev-report 5876378930 on #20462 (the newest there); the edited PR #20490 body, diffed against the body the PASS read; git diff 08ebcd01d 927957df4, separated into the round's own commit 4265c40ca and the merge of origin/main fc0db22bc (read-only git fetch, git log, git diff, git rev-parse on blobs); the check-runs on the new head, read last at 2026-09-28T18:51:38Z. No worktree, checkout, build, test, gate or ablation. Every judgment of the PASS record that this delta does not name carries over unchanged, for the reason ①.1 gives.

① Derived judgments

  1. The PR's own delta is commit 4265c40ca: 2 files, +7 / −3. .changeset/20462-zh-cn-object-labels.md changes one bullet's class list (the word "placeholders" removed, "and" moved); objects-zh-cn-echo-decisions.test.ts changes lines 30–37 of its header comment (+4 comment lines; the file is 324 lines, still 10 it( blocks). No other file is in that diff. The bundle zh-CN.objects.generated.ts, the provenance companion, the nine panel ledgers and the ledger's test bodies are blob-identical at 08ebcd01d, 4265c40ca and 927957df4 (verified by blob id for all 13 files). No translated value, assertion or floor moved, so ① items 1–8, ② and ③ 1–9 of the PASS record stand on this head. RIGHT.
  2. Correction 1, the changeset class list: "labels, plural labels, select options, help, descriptions and empty states" is TRUE against the base-to-head walk (label 169, options 11, pluralLabel 11, help 114, description 11, emptyState 4, sum 320; placeholder 0). The 42-bullet still names "placeholders that show a value the admin types", TRUE (four echo rows).
  3. Correction 2, the ledger header: it now says the table "kept 42 rows, one per echo below (measured equal at the time)", that what is asserted is one direction per verdict (every declared echo still has its row; no pinned translation has one), and that "nothing BEYOND the echoes" is not asserted. TRUE to the letter of the tests: the echo loop asserts table[objects.path] equals hashSource(en), the translate check asserts stillFilled equals [], and the only size assertion is greater than 0. Comment only; the assertions are byte-unchanged.
  4. Correction 3a, the PR body's H4 sentence: "help, description and emptyState were translated, and all four placeholders are declared echoes, because each shows a value the admin types" TRUE (walk: placeholder 4 before, 4 after; the four are the scopes / mapEmail / mapName / identifierFormat placeholders, all echo rows with the typed-value departure).
  5. Correction 3b, the ledger-history sentence: "report-dataset-panel-… is the first: it has Verdict, a reason per row and the per-locale departure check written inline. The named undeclaredEchoes predicate arrives in the later rounds, first in object-field-editor-panel-echo-decisions.test.ts." TRUE: git log -S undeclaredEchoes over the ledgers directory, oldest first, begins at 576d5df66 (fix(platform-objects): decide the object field-editor panel en-echoes per leaf, and derive the panel pin #19418, 2026-09-20), whose diff adds object-field-editor-panel-echo-decisions.test.ts with the predicate; report-dataset-panel- declares departures per locale and checks them inline at lines 193–200. The shared clone is not shallow, so the log is complete.
  6. The merge: origin/main moved 851af0c27 to 811376302 (build(spec): drop the DTS pass's duplicate type check — 93% to 83% of its 6144 MB ceiling #20483, build(spec)), 3062e5001 (fix(core,objectql)!: a date or datetime names a year from 0001 to 9999, refused at the comparand door and the write door (#20264) #20469, fix(core,objectql)!, card temporal values outside the years a four-digit text or a backend holds: a datetime comparand for year 10000 or −1 misorders on memory/SQLite and 500s on PostgreSQL; a date in year 0000 500s on PostgreSQL; a date write stores +010000-… verbatim #20264) and fc0db22bc (fix(driver-sql): sum / avg accumulate in double on PostgreSQL and MySQL, as on SQLite and the rows path #20486, fix(driver-sql)); 21 files changed on main. Their intersection with this PR's 13 files is empty; main's five .changeset/ files belong to cards 20203, 20240, 20263, 20264 and 20387, and nothing on main touches platform-objects/src/apps/translations. At 927957df4: every PR file equals its 4265c40ca blob, every main-changed file equals its fc0db22bc blob, no file differs from both parents, and git diff fc0db22bc 927957df4 is exactly the 13 files (+728 / −659). A true two-parent merge with no hand edit; the merge-base with origin/main is fc0db22bc itself. RIGHT. fix(core,objectql)!: a date or datetime names a year from 0001 to 9999, refused at the comparand door and the write door (#20264) #20469's ! is a breaking change on main's own surface (core / objectql date-year range) and does not move this PR's package or level.

Gate verdicts, final read 2026-09-28T18:51:38Z: 40 check-runs on the head — the body edit at 18:47 re-triggered the PR-metadata workflows, so seven names appear twice — 29 success, 5 skipped (the second Auto Label and Check PR Size runs, Build Docs, Console Pin Gate, Packed-tarball smoke), 6 in progress, 0 failed. In progress: Lint & Repo Gates (the check:i18n family) and Test Core 1/6, 2/6, 4/6, 5/6, 6/6; recorded as in progress. Success: Test Core 3/6, Type Check workspace / source / consumer / debt-ledger, TypeScript Type Check, Check Changeset (both runs), Build Core, Dogfood Regression 1–3 and rollup, Dogfood Verify CLI, Temporal Conformance, Governed Surface Queue Guard, the claim / single-writer / part-of guards (both runs), Flag docs, Check Documentation Links, filter. The round's own delta is a changeset sentence and a comment, on which no gate family can move; the in-progress families are the ones the merge of main re-exercises, and the required-check set holds the landing until they complete.

② Semver level

Unchanged: @objectstack/platform-objects: patch, the Clause-② line no. The delta publishes nothing new (a changelog sentence and a test comment), so the level stands. Changeset sentences: every sentence the PASS marked TRUE is unchanged and still TRUE; the one it marked FALSE now reads "labels, plural labels, select options, help, descriptions and empty states", TRUE. Check Changeset is green on the head.

PR-body sentences changed this round: the H4 sentence TRUE (①.4); the ledger-history sentences TRUE (①.5). Patch-round-1 section, sentence by sentence: "The contract review 5876239898 (PASS at 08ebcd0) named one false word and two comment slips. All three are corrected here, as text only; no translated value, assertion or floor changed." TRUE (①.1). The changeset bullet TRUE. The ledger-header bullet TRUE, including "comment only" and "measured, and deliberately not asserted". The "This body" bullet TRUE (both edits are as diffed). "origin/main fc0db22 was merged with a true merge commit" TRUE (①.6). "At head 927957d, pnpm check:i18n exited 0 and … objects-zh-cn-echo-decisions.test.ts passed 10 of 10" — the dev's local readings, not re-measured here; consistent with the file's 10 tests and with the inputs check:i18n reads being byte-unchanged; CI's Lint & Repo Gates is in progress at the final read. "The round-0 gate readings above were taken at 08ebcd0 and were not re-run." TRUE, and consistent with the report's not_measured. The os-dev-report's own claims (files_changed two files; line budget 2 files +7 / −3 and 13 files +728 / −659; git log -S at 576d5df, not a shallow clone; fast-forward push to 927957d; no open questions) each TRUE against the measurements above.

③ Boundary flags

  1. The three slips from the PASS record: each corrected, each correction TRUE (①.2–①.5). Closed.
  2. The report's open_questions and out_of_scope_findings are empty this round; the round-0 items (the ja-JP / es-ES finding with reach: same public door, the "Locked Until" note, the feat(spec): curated activityMilestones, publicSharing, userActions and inlineColumns form rows (#19332, flight G2b) #20485 overlap) stand as the PASS record answered them. The feat(spec): curated activityMilestones, publicSharing, userActions and inlineColumns form rows (#19332, flight G2b) #20485 overlap reading carries over unchanged: the three shared ledgers are blob-identical from 08ebcd01d to this head and main did not touch them, so the scratch merge-file result (exit 0 on each) still describes this head.
  3. The merge brought no file of this PR and no hand edit (①.6). Nothing to escalate.
  4. Landing note, not a flag: the head is mergeable and blocked only on the six in-progress required runs; the delta gives no reason to expect a red, and the record does not pre-judge them.

Implemented-by: claude/issue-20462-zh-cn-object-labels
Reviewed-by: session_01N8TPEsoJxPsdSdNKGnNGEN

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 18:59
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit 9bf5e67 Sep 28, 2026
43 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20462-zh-cn-object-labels branch September 28, 2026 19:21
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
…ptions and help that shipped as English copies (objectstack-ai#20530)

Fixes objectstack-ai#20493

Clause-②: no

## What changed

A ja-JP or es-ES console read English on Setup surfaces: the Invite user
dialog's role list carried **Delegated Admin** beside the translated
roles, and every Setup record page read the same copied leaves. The keys
were present in
`packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts`
and `es-ES.objects.generated.ts`, but their values were byte copies of
the `en` source that `os i18n extract --fill=default` seeds. This PR
applies the zh-CN mechanism of PR objectstack-ai#20490 (squash `9bf5e67af`) to both
locales, one PR, one ledger per locale.

- **ja-JP: 340 copied leaves translated**, 43 kept as written by design.
- **es-ES: 338 copied leaves translated**, 54 kept as written by design.
- Leaf values only; the structure of both bundles is untouched. `pnpm
i18n:extract` re-read both bundles byte-identical.
- **Two ledgers**, one per locale, of the shape
`objects-zh-cn-echo-decisions.test.ts` built (`type Verdict =
'translate' | 'echo'`, a reason on every row, a departure on every echo,
`undeclaredEchoes` proved on a synthetic row):
`objects-ja-jp-echo-decisions.test.ts` and
`objects-es-es-echo-decisions.test.ts`. No new mechanism.
- **Provenance companions** `ja-JP.source-hashes.generated.ts` and
`es-ES.source-hashes.generated.ts` regenerated by `pnpm i18n:extract`,
never by hand: −340 and −338 rows, 0 added. The rows that remain are
set-equal to each ledger's declared echoes (43 and 54).
- **One sibling ledger followed:**
`object-lifecycle-panel-echo-decisions.test.ts` pinned
`sys_oauth_resource.fields.access_token_ttl.label` as an unauthored fill
in ja-JP and es-ES. Both are now authored (アクセストークン TTL, TTL del token
de acceso), so the test now asserts all three locales authored with the
`TTL` token kept and no provenance row. The other eight sibling ledgers
already asked for a non-empty table (`> 0`) since PR objectstack-ai#20490 and needed
no edit.
- zh-CN, `*.metadata-forms.generated.ts`, the hand-authored `ja-JP.ts` /
`es-ES.ts` and objectui are not touched.

**The coverage-ratchet half is out of scope, per triage (5877552039,
「新增门禁默认否」).** Teaching `i18n-coverage-baseline` to count a copied source
string as untranslated widens an existing gate; if the maintainer wants
that ratchet, it is a separate gate card. For the same reason neither
ledger asserts that no other leaf of its locale is a copy.

## Pinned strings

| leaf | ja-JP | es-ES | pin |
|---|---|---|---|
| `sys_member.fields.role.options.delegated_admin` | 委任管理者 |
Administrador delegado | exact word, equal to the console's |
| `sys_invitation.fields.role.options.delegated_admin` | 委任管理者 |
Administrador delegado | equal to the `sys_member` options, one word per
role |
| `sys_team.fields.member_count.label` | メンバー数 | Número de miembros |
not the `en` source, no provenance row |

The console's words were read at objectui `main` `797a30f`, read-only:
`packages/i18n/src/locales/ja.ts`, key
`organization.roles.delegatedAdmin` = 委任管理者;
`packages/i18n/src/locales/es.ts`, same key = Administrador delegado.
The role test also asserts the four role words are distinct, identical
on both objects, Japanese-script in ja-JP and none equal to its `en`
source in es-ES.

## Copy count, before and after, by key class

A string leaf counts as a copy when it is byte-equal to the `en` leaf at
the same path, over every leaf of `en.objects.generated.ts` and the
locale's bundle. All three bundles have 1529 leaves. "Before" is
measured at base `397572ed5`; the card's `b810ddb6f` counts are
reproduced exactly (H1 holds).

| key class | ja-JP before | ja-JP after | es-ES before | es-ES after |
|---|---:|---:|---:|---:|
| `label` (object, field, view, action param) | 210 | 29 | 217 | 38 |
| `options.*` | 20 | 10 | 22 | 12 |
| `pluralLabel` | 12 | 0 | 12 | 0 |
| `help` | 121 | 0 | 121 | 0 |
| `description` | 12 | 0 | 12 | 0 |
| `placeholder` | 4 | 4 | 4 | 4 |
| `emptyState.title` / `.message` | 4 | 0 | 4 | 0 |
| **total** | **383** | **43** | **392** | **54** |
| `label` + `options.*` | 230 | 39 | 239 | 50 |

`help` / `description` / `placeholder` / `emptyState` were decided as
the same class as `label` (H5), as zh-CN did: the extractor files each
under the same `source` kind as its element's label
(`packages/cli/src/utils/i18n-extract.ts`). All four placeholders in
both locales are typed literals and stay as written.

## Kept as written by design — ja-JP (43)

| leaves | value(s) | reason |
|---|---|---|
| `sys_account._actions.link_social.params.provider.options.{google,
github, microsoft, apple, facebook, gitlab, discord}` (7) | Google,
GitHub, Microsoft, Apple, Facebook, GitLab, Discord | Sign-in provider
brands. The bundle renders no brand name in kana: the authored SSO empty
state keeps Okta, Entra and Auth0 verbatim. |
| `fields.id.label` on the 23 objects zh-CN lists (`sys_oauth_*` ×7,
`sys_sso_provider`, the eight `sys_scim_*`, `sys_email_template`,
`sys_metadata`, `sys_metadata_history`, `sys_view_definition`,
`sys_metadata_audit`, `sys_secret`, `sys_setting_audit`) (23) | ID | The
bundle keeps ID verbatim in every label that carries it (ユーザー ID, 組織 ID,
クライアント ID); a bare ID has no noun to render. |
| `sys_oauth_application.fields.jwks.label`, `.jwks_uri.label` (2) |
JWKS, JWKS URI | Protocol names: `sys_jwks` is authored 署名キー (JWKS), and
URIs stay URI (リダイレクト URI). Their `help` leaves are translated. |
|
`sys_oauth_application._actions.create_oauth_application.params.type.options.web`
(1) | Web | The bundle already renders the sibling
`sys_oauth_application.fields.type.options.web` as Web; the dialog and
the record field read one word. |
|
`sys_sso_provider._actions.register_saml_provider.params.entryPoint.label`
(1) | IdP SSO URL | Protocol initialisms only; the sibling SAML params
are authored IdP エンティティ ID and IdP 署名証明書. |
| `sys_sso_provider._actions.register_sso_provider.params.{scopes,
mapEmail, mapName}.placeholder` (3) | openid email profile, email, name
| Typed literals: OIDC scope and claim names; the authored help on the
same params keeps them (既定値は「email」). |
|
`sys_sso_provider._actions.register_saml_provider.params.identifierFormat.placeholder`
(1) | the SAML NameID emailAddress URN | Typed literal: an identifier,
not prose. |
| `sys_email.fields.message_id.label` (1) | Message-ID | RFC 5322 header
name; the authored help keeps it (トランスポートが割り当てる RFC-5322 Message-ID). |
| `sys_email.fields.cc_addresses.label`, `.bcc_addresses.label` (2) |
Cc, Bcc | Header abbreviations Japanese mail clients print in Latin
letters, with no kana or kanji form in use. The prose labels on the same
record are authored (送信元, 宛先, and 返信先 for Reply-To in this PR). |
| `sys_setting_audit.fields.source.options.{ui, api}` (2) | UI, API |
Initialisms the bundle keeps (API キー, UI には表示されません;
`sys_metadata.fields.source.options.api` is authored API). |

Where ja-JP departs from zh-CN: the result dialog's `Client ID` /
`Client Secret` were never copies here (authored クライアント ID /
クライアントシークレット); `Web` and `Cc` / `Bcc` are ja-JP decisions of their own.

## Kept as written by design — es-ES (54)

| leaves | value(s) | reason |
|---|---|---|
| the 7 sign-in provider options | Google … Discord | Brands; the
authored SSO empty state keeps Okta, Entra and Auth0 verbatim. |
| `fields.id.label` on the same 23 objects (23) | ID | The bundle keeps
ID verbatim in every label that carries it (ID de usuario, ID de
organización, ID de cliente). |
| `sys_oauth_application.fields.jwks.label`, `.jwks_uri.label` (2) |
JWKS, JWKS URI | `sys_jwks` is authored Clave de firma (JWKS); URIs stay
URI (URI de redirección). |
| `...create_oauth_application.params.type.options.web` (1) | Web | The
sibling `fields.type.options.web` is authored Web. |
| the 3 OIDC placeholders + the SAML NameID URN placeholder (4) | openid
email profile, email, name, URN | Typed literals; the authored help
keeps them (Valor predeterminado: «email»). |
| `sys_email.fields.message_id.label` (1) | Message-ID | Authored help:
Message-ID RFC-5322 asignado por el transporte. |
| `sys_email.fields.cc_addresses.label` (1) | Cc | The Spanish copy
abbreviation (con copia) is Cc too; the blind-copy sibling is authored
Cco. |
| `sys_setting_audit.fields.source.options.{ui, api}` (2) | UI, API |
Initialisms the bundle keeps (nunca se muestra en la UI; `sys_metadata`
source option `api` is authored API). |
| `sys_email.fields.error.label`, `sys_job_run.fields.error.label` (2) |
Error | Spanish spells it the same (el error); the bundle writes Último
error on `sys_job_queue`. |
| `sys_notification.fields.actor_id.label`,
`sys_metadata_audit.fields.actor.label`,
`sys_setting_audit.fields.actor_id.label` (3) | Actor | Spanish spells
it the same (el actor); authored help reads Último actor que escribió
esta fila, and the messaging bundle authors its actor label Actor. |
| `sys_setting.fields.scope.options.global`,
`sys_setting_audit.fields.scope.options.global` (2) | Global | Spanish
spells it the same, beside the authored sibling options Inquilino and
Usuario. |
| `sys_email_template.fields.variables_json.label` (1) | Variables
(JSON) | Spanish spells it the same; the authored description of the
same object says (asunto + contenido + variables). |
| `sys_oauth_access_token.fields.token.label`,
`sys_oauth_refresh_token.fields.token.label` (2) | Token | The bundle's
own word for the concept (Token de sesión, Token de acceso). |
| `sys_metadata.fields.checksum.label`,
`sys_metadata_history.fields.checksum.label` (2) | Checksum | The
bundle's own word (Checksum anterior). |
| `sys_organization.fields.slug.label` (1) | Slug | The bundle's own
word (Cambiar slug); the console says the same (Ese slug ya está en
uso). |

Where es-ES departs from zh-CN: `Client ID` / `Client Secret` in the
result dialog and `IdP SSO URL` were never copies here (authored); the
14 Spanish-as-written rows above are es-ES's own class.

## Terminology (H4: one term, one rendering)

- **ja-JP** takes the bundle's already-authored words: 作成日時 / 更新日時 /
有効期限 / 失効日時 / ローテーション日時, クライアント ID, スコープ, クレーム (claim, as in マッピング:
メールクレーム), プロビジョニング (as in IdP プロビジョニング), 投影 and 付与 (as
`sys_scim_projection_grant.display_title` authors them), リース and 冪等キー
(`sys_job_queue`), トピック / 重大度 / 情報・警告・重大 / 操作者 (the messaging bundle),
ポジション (the security bundle), 管理者 for `Managed By` (as `sys_metadata`),
所属組織 for My Memberships (the account nav's label, `ja-JP.ts`
`nav_account_memberships`), 配属 for Placement (objectui's word).
Decommission is 廃止; tombstone is トゥームストーン, the service-storage ja-JP
objects bundle's existing rendering.
- **es-ES** likewise: Creado el / Actualizado el / Caduca el / Revocado
el / Rotado el, ID de cliente, Ámbitos, reclamo (claim, as in Mapeo:
reclamo de correo), aprovisionamiento (as in Aprovisionada por IdP),
concesión (as in Tipos de concesión), arrendamiento (`sys_job_queue`'s
lease), Tema / Severidad / Información · Advertencia · Crítica (the
messaging bundle), Puesto (the security bundle), principal for Parent
(the bundle's word throughout: Unidad de negocio principal, Objeto
principal), Mis organizaciones (`es-ES.ts` `nav_account_memberships`),
Asignación for Placement (objectui's word). Decommission is retirada;
tombstone is lápida.

## Verification

Measured at head `d616c8cc3`, a true merge of `origin/main` `fb386074f`
(parents `8f3e7cae3`, `fb386074f`). The CLI closure was rebuilt after
the merge.

- **H2, `pnpm check:i18n`:**
  - exit 0 with both bundles restored to their `397572ed5` bytes;
- exit 1 after the leaf edits alone, `platform-objects DRIFTED (2)`: the
two provenance companions (zh-CN's run read `DRIFTED (1)`, one locale);
- exit 0 after `pnpm i18n:extract`, which changed only the two
companions (−340 / −338 rows, 0 added). Remaining rows vs remaining
copies: 43 = 43 and 54 = 54, set difference 0. The mechanism carried
over unchanged; the only per-locale difference is which leaves each
locale keeps.
  - exit 0 again at `d616c8cc3` (derived gate run below).
- **`pnpm --filter @objectstack/platform-objects exec vitest run
--maxWorkers=2`:** 58 files, 942 tests passed (lock verdict command-exit
0), at `d616c8cc3` and before the merge.
- **`pnpm --filter @objectstack/platform-objects typecheck`:** exit 0.
`tsc -p tsconfig.test.json --listFilesOnly` lists both new ledgers (2
hits).
- **Reverse verification.** Five mutation legs through
`scripts/ablation-replace.mjs` (anchor hit counts declared, blob change
proved, restore proved blob == HEAD and `git diff HEAD` empty each
time), each red as predicted:
1. ja-JP both `delegated_admin: "委任管理者"` → `"Delegated Admin"`: 2 failed
/ 9 passed (verdict row; role test "expected 'Delegated Admin' to match"
the Japanese-script pattern).
2. es-ES both `delegated_admin: "Administrador delegado"` → `"Delegated
Admin"`: 2 failed / 8 passed (verdict row; "es-ES role delegated_admin
reads its en source").
3. ja-JP declared echo `google: "Google"` → `"グーグル"`: 1 failed / 10
passed ("is a declared echo and must stay the en source").
4. es-ES declared echo `global: "Global"` (×2) → `"Mundial"`: 1 failed /
9 passed.
5. ja-JP `委任管理者` (×2) → `委託管理者`, still Japanese: 1 failed / 10 passed,
only the console-word pin ("expected '委託管理者' to be '委任管理者'").
6. Sibling leg: `object-lifecycle-panel-echo-decisions.test.ts` restored
to its `397572ed5` bytes against the new bundles: 1 failed / 34 passed
("ja-JP sys_oauth_resource.fields.access_token_ttl.label: expected
'アクセストークン TTL' to be 'Access Token TTL'"); restored to HEAD, blob equal.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at `d616c8cc3` printed 61 commands.
All 61 were run, each exit code written to a file before any pipe.
  - 60 exited 0 on their first run.
- `pnpm check:dual-build-cjs-loads` first answered PREREQUISITE NOT MET
(exit 3: `dist/` missing for packages outside the CLI closure). Not a
measurement. Rerun after a workspace build (71 of 71 tasks cached): exit
0.
- `--ran` reconciliation: 61 derived, 61 run, 0 NOT-MEASURED, 0 UNRUN
(exit 0).
- **`pnpm check:i18n-coverage`:** exit 0, "621 baselined untranslated
string(s), none new". The ratchet is unchanged, as expected.
- **ESLint, narrowed and declared:** `eslint --no-inline-config --format
json` over the 7 changed `.ts` files: 7 files linted (each confirmed not
ignored by the repo config's own `isPathIgnored`), 0 errors, 0 warnings.
`eslint.config.mjs` never enables type-aware linting (no
`parserOptions.project`, stated in its own comment), so this diff cannot
move a verdict on any untouched file. The repo-wide `pnpm lint` is CI's.
- **H6, size:** the derivation reads 2787 changed lines (+1416 / −1371)
over 8 files, under the 5000 human-merge threshold.
- **NOT MEASURED locally, declared to CI:** the 6 workflow-valued
families, the 11 wide-population families and the path-scheduled CI jobs
the derivation names. The Invite dialog was not re-driven in a browser
(NOT MEASURED: browser rendering; reason: bundle leaves are read
directly, as the card measured them).

## Acceptance notes

- es-ES renders `Parent` as principal throughout the bundle, so the
pre-existing `sys_business_unit.fields.parent_business_unit_id.label`
and `sys_user.fields.primary_business_unit_id.label` both read Unidad de
negocio principal. This PR follows the bundle's word for `Parent
Organization` (Organización principal) and leaves the pre-existing pair
alone. Carrier: none.
- The ja-JP bundle renders `Locked Until` two ways already (ロック解除日時 on
`sys_user`, ロック期限 on `sys_job_queue`). This PR uses ロック解除日時 for
`sys_two_factor`, the same lockout concept as `sys_user`.
- ja-JP `Managed By` is 管理者, `sys_metadata`'s word, so
`sys_email_template` reads 管理者 as the label and as the `admin` option.
管理元 would disambiguate; one line changes it if the maintainer prefers
it.
- A few authored (not copied) ja-JP / es-ES leaves no longer match their
moved `en` source, for example `sys_two_factor.fields.backup_codes.help`
(still "JSON-serialized backup codes" where `en` now says an opaque
ciphertext) and `sys_notification.description`. Authored leaves are
legacy-trusted by the provenance design and outside this card's copy
class. Carrier: none.
- The eight sibling ledgers' comments name only zh-CN ("since objectstack-ai#20462
zh-CN's objects slice holds only its declared English-by-design
leaves"); the `> 0` floors they guard hold for all three locales now.
Comment-only drift, not edited here.

---

_Generated by [Claude
Code](https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

platform-objects: the zh-CN bundle ships 192 labels and options as the English source string, and the i18n coverage baseline counts them as translated

2 participants